// *********************************************************
//      Copyright (c) Kosta Gaitanis
//      All rights reserved
//
//      k.gaitanis@gmail.com
//
// *********************************************************

#include "Vector.h"
#include "Camera.h"

Camera::Camera(QObject* parent) :
    QObject(parent),
    m_cameraPosition(0.0, 0.0, 1.0),
    m_targetPosition(0.0, 0.0, 0.0),
    m_upDirection(0.0, 1.0, 0.0),
    m_fov(60.0),
    m_aspectRatio(1.0),
    m_near(0.01),
    m_far(10000.0)
{
    calculateCameraMatrix();
}

Camera::~Camera()
{
}

void Camera::initializeGL()
{
    GLdouble fW, fH;
    //START//

    // Clear the rendering window
    // the color buffer and depth buffer should be emptied.
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // calculate frustum width and height using FieldOfView, AspectRatio and near
    fH = tan(m_fov / 360.0 * M_PI) * m_near;
    fW = fH * m_aspectRatio;    

    // Switch to projection matrix mode
    glMatrixMode(GL_PROJECTION);

    // load identity matrix
    glLoadIdentity();

    // call glFrustum with correct parameters
    // (0,0) is at the center of the projection plan
    // topleft coordinate is (-fW, fH)
    // bottom right is (fW,-fH)
    // use m_near and m_far
    glFrustum(-fW, fW, -fH, fH, m_near, m_far);

    //END//

    if (glGetError() == GL_INVALID_OPERATION) {
        qDebug() << "setting glFrustum is not allowed between glBegin and glEnd calls.";
    }

    // this is only used to store the values you calculated and test if they are correct.
    setFrustumSize(fW, fH);
}

void Camera::calculateCameraMatrix()
{
    Matrix m;
    //START//
    // calculate the x,y and z vectors of the initial model view
    // matrix using camera position, target position and up direction
    Vector z = (m_targetPosition - m_cameraPosition).normalized();
    Vector x = (z ^ m_upDirection).normalized();
    Vector y = (x ^ z).normalized();

    // create a transformation matrix using these vectors and store it in m
    m = Matrix(x.x(), x.y(), x.z(), 0,
               y.x(), y.y(), y.z(), 0,
               -z.x(), -z.y(), -z.z(), 0,
               0, 0, 0, 1);

    // translate the matrix by the camera position
    m = m * Matrix::translation(-m_cameraPosition);
    //END//

    // this is only used to store the values you calculated and test if they are correct.
    // the matrix you stored can be retrieved using getCameraMatrix()
    setCameraMatrix(m);
}

void Camera::paint()
{
    //START//
    // convert the camera Matrix into an openGL array of floats.
    GLdouble position[16];
    getCameraMatrix().toGLArray(position);

    // Switch to model view matrix mode
    glMatrixMode(GL_MODELVIEW);
    // Load this matrix
    glLoadMatrixd(position);
    //END//
}

void Camera::zoom(float zoomFactor)
{
    // move the camera closer/further to the target

    //START//
    // calculate the direction of the camera
    Vector direction = m_targetPosition - m_cameraPosition;

    // multiply that direction by the zoomFactor
    direction = direction*(zoomFactor + 1.0);

    // calculate the new position of the camera
    // the direction of the camera is the same, the target
    // is at the same position, but the camera position has changed
    Vector new_position = m_targetPosition - direction;

    // apply the new camera position using setCameraPosition()
    setCameraPosition(new_position);
    //END//
}

void Camera::rotateCameraAroundTarget(float theta, float phi)
{
    //START//
    // calculate the direction where the camera is looking
    Vector direction = m_targetPosition - m_cameraPosition;

    // theta is the angle to rotate the camera
    // in a plane that is vertical to up
    // keeping the same up direction
    Matrix v_m = Matrix::rotation(-theta, m_upDirection);

    // phi rotates the camera around the center towards the up direction
    // thus modifying the up vector
    Vector n(direction^m_upDirection);
    Matrix h_m = Matrix::rotation(-phi,n);

    // apply both transformations to the direction vector
    direction = h_m*(v_m*direction);

    // calculate the new position of the camera
    Vector new_position = m_targetPosition - direction;

    // calculate the new up direction of the camera
    Vector new_up = h_m*m_upDirection;

    // apply the transformations using setCameraPosition() and setUpDirection()
    setCameraPosition(new_position);
    setUpDirection(new_up);
    //END//
}

void Camera::rotateCamera(float theta)
{
    //START//
    // calculate the camera direction vector
    Vector direction = m_targetPosition - m_cameraPosition;

    // create a rotation matrix around this vector
    Matrix m = Matrix::rotation(theta, direction);

    // rotate the camera up vector using the matrix
    Vector new_up = m*m_upDirection;

    // set new up vector for camera using setUpDirection()
    setUpDirection(new_up);
    //END//
}

void Camera::setCameraPosition(const Vector &position)
{
    m_cameraPosition = position;    
    calculateCameraMatrix();
    emit changed();
}

void Camera::setTargetPosition(const Vector &position)
{
    m_targetPosition = position;
    calculateCameraMatrix();
    emit changed();
}

void Camera::setUpDirection(const Vector &up)
{
    m_upDirection = up.normalized();
    calculateCameraMatrix();
    emit changed();
}

void Camera::setCameraPosition(double x, double y, double z)
{
    setCameraPosition(Vector(x,y,z));
}

void Camera::setTargetPosition(double x, double y, double z)
{
    setTargetPosition(Vector(x,y,z));
}

void Camera::setUpDirection(double x, double y, double z)
{
    setUpDirection(Vector(x,y,z));
}

void Camera::setFieldOfView(double degrees)
{
    if (degrees >= 180) {
        qDebug() << "Field of view must be smaller than 180.";
        degrees = 179;
    } else if (degrees <= 0) {
        qDebug() << "Field of view must be greater than 0.";
        degrees = 10;
    }
    m_fov = degrees;
    initializeGL();
    emit changed();
}

void Camera::setAspectRatio(double ratio)
{
    if (ratio <= 0.0) {
        ratio = 1.0;
    }
    m_aspectRatio = ratio;
    initializeGL();
    emit changed();
}

void Camera::setNearLimit(double nearLimit)
{
    if (nearLimit <= 0.0) {
        nearLimit = 0.00001;
    }
    m_near = nearLimit;
    initializeGL();
    emit changed();
}

void Camera::setFarLimit(double farLimit)
{
    if (farLimit <= m_near) {
        farLimit = m_near;
    }
    m_far = farLimit;
    initializeGL();
    emit changed();
}

double Camera::getFieldOfView() const
{
    return m_fov;
}

double Camera::getAspectRatio() const
{
    return m_aspectRatio;
}

double Camera::getNearLimit() const
{
    return m_near;
}

double Camera::getFarLimit() const
{
    return m_far;
}

Vector Camera::getCameraPosition() const
{
    return m_cameraPosition;
}

Vector Camera::getTargetPosition() const
{
    return m_targetPosition;
}

Vector Camera::getUpDirection() const
{
    return m_upDirection;
}

Matrix Camera::getCameraMatrix() const
{
    return m_cameraMatrix;
}

QSizeF Camera::getFrustumSize() const
{
    return QSizeF((float)m_frustumWidth, (float)m_frustumHeight);
}

void Camera::onWindowResized(int width, int height)
{
    // set the aspect ratio of the camera to width/height
    setAspectRatio( (double) width / (double) height);
    glViewport(0.0, 0.0, width, height);
}

void Camera::setFrustumSize(GLdouble w, GLdouble h)
{
    m_frustumWidth = w;
    m_frustumHeight = h;
}

void Camera::setCameraMatrix(Matrix cameraMatrix)
{
    m_cameraMatrix = cameraMatrix;
}
